Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/santiagodc8/tu_perfil.net/llms.txt

Use this file to discover all available pages before exploring further.

Article pages are the primary reading destination on TuPerfil.net. Each article is served at /noticia/{slug} and revalidates every 5 minutes.
src/app/(public)/noticia/[slug]/page.tsx
export const revalidate = 300; // Revalidate every 5 minutes

URL pattern

/noticia/{slug}
Examples:
  • /noticia/candidato-anuncia-su-postulacion
  • /noticia/nuevo-hospital-regional-inaugurado
If the slug does not match any published article, Next.js renders a 404 via notFound().

The Article interface

The canonical TypeScript type for an article is defined in src/types/index.ts:
src/types/index.ts
export interface Article {
  id: string;
  title: string;
  slug: string;
  content: string;         // Rich HTML from TipTap
  excerpt: string;
  image_url: string | null;
  category_id: string;
  published: boolean;
  published_at: string | null;
  featured: boolean;       // Shown in hero carousel
  views: number;           // Cumulative view count
  created_at: string;
  updated_at: string;
  author_id: string;
  author_name: string;
  deleted_at: string | null;
  gallery: string[];       // Array of image URLs
  // Joined fields
  category?: Category;
  tags?: Tag[];
}

What the reader sees

The article page layout, from top to bottom:
1

Breadcrumbs

A trail showing Home › Category name › Article title. The category link navigates to the category page.
2

Category badge and metadata

A colored pill badge for the category, a “Por ” byline linking to /autor/{slug}, the publication date (formatted as relative time via smartDate), and the estimated reading time via readingTime.
3

Title

The article <h1>, rendered in a large, bold typeface.
4

Share buttons (top)

Social sharing buttons via the ShareButtons component — allows readers to share the article URL.
5

Cover image

If image_url is set, the cover image is rendered in a 16:9 aspect-ratio container with priority loading and a blur placeholder.
6

Gallery

If the gallery array has entries, an ImageGallery component renders the additional images below the cover.
7

Article body

The rich HTML content is rendered by the ArticleBody component, which also includes reading controls (font size adjustment). The body uses Tailwind’s prose classes for typography.
8

Share buttons (bottom)

A second set of share buttons appears after the article body, above the tags.
9

Tags

Pill links for each tag associated with the article, each linking to /etiqueta/{tag-slug}.
10

Ad banner

One active between_articles or header ad is displayed after the tags.
11

Comments

A comment list (approved comments only) followed by a comment submission form.
12

Related articles

Up to 4 related articles displayed in a wide grid below the main content.

Floating WhatsApp button

On mobile, a floating WhatsApp share button (FloatingWhatsApp) appears fixed at the bottom of the screen, providing a quick way to share the article via WhatsApp.

View count tracking

Every time a reader opens an article page, the ViewCounter client component fires a POST request to /api/views/{article_id} on mount:
src/components/public/ViewCounter.tsx
"use client";

import { useEffect } from "react";

export default function ViewCounter({ articleId }: { articleId: string }) {
  useEffect(() => {
    const referrer = document.referrer || "";
    fetch(`/api/views/${articleId}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ referrer }),
    });
  }, [articleId]);

  return null;
}
The API route calls the increment_views Supabase RPC, which atomically increments the article’s views counter and inserts a row into page_views with the referrer source:
src/app/api/views/[id]/route.ts
export async function POST(request, { params }) {
  const { error } = await supabase.rpc("increment_views", {
    article_id: params.id,
    p_referrer: referrer || null,
    p_referrer_source: referrerSource,
  });
}
The referrer URL is classified into one of these sources: direct, google, facebook, twitter, whatsapp, instagram, tiktok, telegram, or other. This data powers the traffic analytics visible in the admin panel. The page fetches up to 4 related articles using a two-step strategy:
  1. Tag-based: The related_articles_by_tags RPC finds articles that share the most tags with the current article, within the same category.
  2. Category fallback: If fewer than 4 tag-related articles are found, the remainder is filled with recent articles from the same category.
src/app/(public)/noticia/[slug]/page.tsx
const { data: tagRelated } = await supabase.rpc("related_articles_by_tags", {
  p_article_id: article.id,
  p_category_id: article.category_id,
  lim: 4,
});
Related articles are shown in a wide 4-column grid (RelatedArticles component) below the comments section.

Comments

Readers can leave comments on any published article. Comments require approval before they appear publicly.

Submitting a comment

The CommentForm client component collects name, email, and message, then posts to POST /api/comments:
src/components/public/CommentForm.tsx
const res = await fetch("/api/comments", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    article_id: articleId,
    author_name: name.trim(),
    author_email: email.trim(),
    content: content.trim(),
  }),
});
Validation rules (server-side):
  • All fields (article_id, author_name, author_email, content) are required.
  • author_name must be at least 2 characters.
  • author_email must match a basic email regex.
  • content must be between 5 and 2,000 characters.
  • A rate limit of 10 comments per IP per hour is enforced (returns HTTP 429 if exceeded).
After a successful submission, the form shows a confirmation message: “Tu comentario fue enviado y será publicado luego de ser aprobado.”

Reading comments

The CommentList component fetches approved comments via GET /api/comments?article_id={uuid}. Only comments with approved = true are returned.
src/app/api/comments/route.ts
const { data } = await supabase
  .from("comments")
  .select("id, author_name, content, created_at")
  .eq("article_id", article_id)
  .eq("approved", true)
  .order("created_at", { ascending: true });
The reader’s email address is never displayed publicly. It is stored in the database for admin reference only.
Admins approve or reject comments from the admin panel at /admin/comentarios.

Structured data (JSON-LD)

Each article page includes a NewsArticle JSON-LD block for search engine rich results:
src/app/(public)/noticia/[slug]/page.tsx
const jsonLd = {
  "@context": "https://schema.org",
  "@type": "NewsArticle",
  headline: article.title,
  description: article.excerpt,
  image: article.image_url ?? undefined,
  datePublished: article.created_at,
  author: {
    "@type": "Person",
    name: article.author_name,
    url: `https://tuperfil.net/autor/${generateSlug(article.author_name)}`,
  },
  publisher: {
    "@type": "Organization",
    name: "TuPerfil.net",
    url: "https://tuperfil.net",
  },
  mainEntityOfPage: articleUrl,
};

RSS feed

The site publishes an RSS 2.0 feed at /feed.xml. It contains up to 20 of the most recently published articles, revalidated every 5 minutes. Each feed item includes the article title, URL, excerpt as description, publication date, category, and author name.
GET https://tuperfil.net/feed.xml
Content-Type: application/rss+xml; charset=utf-8
Cache-Control: public, s-maxage=300, stale-while-revalidate=600
Only articles where published_at <= now() are included — scheduled future articles are excluded.

Sitemap

A dynamic sitemap is generated at /sitemap.xml by src/app/sitemap.ts. It includes:
Page typeChange frequencyPriority
Home (/)hourly1.0
Category pagesdaily0.8
Article pagesweekly0.6
Contact, about, searchmonthly0.2–0.3
Article lastModified uses the updated_at timestamp. Future-scheduled articles (where published_at > now()) are excluded from the sitemap.